/**
 * License would add here
 */

#ifndef UTILS_JS_FUNCTION_H
#define UTILS_JS_FUNCTION_H

#include <node_api.h>

#include <csignal>
#include <cstring>

#include "js_native_api_types.h"
#include "js_object.h"
#include "js_value_base.h"
#include "js_value_ref.h"
#include "log.h"
#include "macros.h"

#define IS_FUNCTION(type) VALUE_TYPE_CHECK(IsFunction(), "function", (type))

class JSFunction : public JSObject {
public:
    JSFunction(const JSString& name,
        const napi_callback& func,
        void* data = nullptr)
        : JSObject(GetEnv(name), true)
        , thisVar_(GetGlobal())
    {
        napi_create_function(
            env_, name.GetValue().c_str(), name.Length(), func, data, &value_);
        UpdateType();
    }
    JSFunction(const JSString& name,
        const JSValue& thisVar,
        const napi_callback& func,
        void* data = nullptr)
        : JSObject(GetEnv(name), true)
        , thisVar_(thisVar)
    {
        NAPI_STATUS_CALL(
            env_,
            napi_create_function(
                env_, name.GetValue().c_str(), name.Length(), func, data, &value_));
        UpdateType();
    }
    JSFunction(napi_env env,
        napi_value name,
        napi_callback func,
        void* data = nullptr)
        : JSObject(env, true)
        , thisVar_(GetGlobal())
    {
        JSString jsName(env, name);
        NAPI_STATUS_CALL(
            env_,
            napi_create_function(
                env_, jsName.GetValue().c_str(), jsName.Length(), func, data, &value_));
        UpdateType();
    }
    JSFunction(const napi_env& env,
        const napi_value& name,
        const napi_value& thisVar,
        const napi_callback& func,
        void* data = nullptr)
        : JSObject(env, true)
        , thisVar_(thisVar)
    {
        JSString jsName(env, name);
        NAPI_STATUS_CALL(
            env_,
            napi_create_function(
                env_, jsName.GetValue().c_str(), jsName.Length(), func, data, &value_));
        UpdateType();
    }
    JSFunction(napi_env env,
        std::string name,
        napi_callback func,
        void* data = nullptr)
        : JSObject(env, true)
        , thisVar_(GetGlobal())
    {
        NAPI_STATUS_CALL(env_,
            napi_create_function(
                env_, name.c_str(), name.length(), func, data, &value_));
        UpdateType();
    }
    JSFunction(const napi_env& env,
        const std::string& name,
        const napi_value& thisVar,
        const napi_callback& func,
        void* data = nullptr)
        : JSObject(env, true)
        , thisVar_(thisVar)
    {
        NAPI_STATUS_CALL(env_,
            napi_create_function(
                env_, name.c_str(), name.length(), func, data, &value_));
        UpdateType();
    }
    JSFunction(const napi_env& env, const napi_value& func)
        : JSObject(env, func)
        , thisVar_(GetGlobal())
    {
        IS_FUNCTION(type_);
    }
    JSFunction(const napi_env& env,
        const napi_value& thisVar,
        const napi_value& func)
        : JSObject(env, func)
        , thisVar_(thisVar)
    {
        IS_FUNCTION(type_);
    }
    JSFunction(const JSValue& value)
        : JSObject(value)
        , thisVar_(GetGlobal())
    {
        IS_FUNCTION(type_);
    };
    JSFunction(const JSFunction& func)
        : JSObject(func)
    {
        thisVar_ = func.thisVar_;
    };
    JSFunction(const JSValue& thisVar, const JSValue& func)
        : JSObject(func)
        , thisVar_(thisVar)
    {
        IS_FUNCTION(type_);
    };

    ~JSFunction()
    {
        thisVar_ = nullptr;
        boundArgc_ = 0;
        delete[] boundArgv_;
        boundArgv_ = nullptr;
    }

    inline JSValue Call()
    {
        JSValue result(env_);
        NAPI_STATUS_CALL(env_,
            napi_call_function(
                env_, thisVar_, value_, boundArgc_, boundArgv_, &result));
        result.UpdateType();
        return result;
    }

    inline JSValue Call(std::initializer_list<JSValue> args)
    {
        size_t argc = args.size();

        napi_value* argv = new napi_value[argc + boundArgc_];
        for (size_t i = 0; i < boundArgc_; i++) {
            argv[i] = boundArgv_[i];
        }
        auto iter = args.begin();
        for (size_t i = 0; i < args.size(); i++) {
            argv[i + boundArgc_] = *iter;
            iter++;
        }

        JSValue result(env_);
        NAPI_STATUS_CALL(env_,
            napi_call_function(
                env_, thisVar_, value_, argc + boundArgc_, argv, &result));
        result.UpdateType();
        delete[] argv;
        return result;
    }
    inline JSValue Call(std::initializer_list<napi_value> args)
    {
        size_t argc = args.size();

        napi_value* argv = new napi_value[argc + boundArgc_];
        for (size_t i = 0; i < boundArgc_; i++) {
            argv[i] = boundArgv_[i];
        }
        auto iter = args.begin();
        for (size_t i = 0; i < args.size(); i++) {
            argv[i + boundArgc_] = *iter;
            iter++;
        }

        JSValue result(env_);
        NAPI_STATUS_CALL(env_,
            napi_call_function(
                env_, thisVar_, value_, argc + boundArgc_, argv, &result));
        result.UpdateType();
        delete[] argv;

        return result;
    }
    inline JSValue Call(size_t argc, JSValue* args)
    {
        napi_value* argv = argc + boundArgc_ > 0 ? new napi_value[argc + boundArgc_] : nullptr;
        for (size_t i = 0; i < boundArgc_; i++) {
            argv[i] = boundArgv_[i];
        }
        for (size_t i = 0; i < argc; i++) {
            argv[i + boundArgc_] = args[i];
        }
        JSValue result(env_);

        NAPI_STATUS_CALL(env_,
            napi_call_function(
                env_, thisVar_, value_, argc + boundArgc_, argv, &result));
        result.UpdateType();
        delete[] argv;

        return result;
    }
    inline JSValue Call(size_t argc, napi_value* args)
    {
        napi_value* argv = argc + boundArgc_ > 0 ? new napi_value[argc + boundArgc_] : nullptr;
        for (size_t i = 0; i < boundArgc_; i++) {
            argv[i] = boundArgv_[i];
        }
        for (size_t i = 0; i < argc; i++) {
            argv[i + boundArgc_] = args[i];
        }

        JSValue result(env_);
        NAPI_STATUS_CALL(env_,
            napi_call_function(
                env_, thisVar_, value_, argc + boundArgc_, argv, &result));
        delete[] argv;

        return result;
    }
    inline JSValue operator()() { return Call(); }
    inline JSValue operator()(std::initializer_list<JSValue> args)
    {
        return Call(args);
    }
    inline JSValue operator()(std::initializer_list<napi_value> args)
    {
        return Call(args);
    }
    inline JSValue operator()(size_t argc, JSValue* args)
    {
        return Call(argc, args);
    }
    inline JSValue operator()(size_t argc, napi_value* args)
    {
        return Call(argc, args);
    }

    inline JSFunction Bind(std::initializer_list<JSValue> args)
    {
        size_t argc = args.size();

        napi_value* argv = new napi_value[argc];
        auto iter = args.begin();

        JSValue thisVar = *iter;
        JSFunction result(thisVar, *this);
        result.boundArgc_ = args.size() - 1;
        iter++;

        for (size_t i = 0; i < result.boundArgc_; i++) {
            argv[i] = *iter;
            iter++;
        }
        result.boundArgv_ = argv;

        return result.As<JSFunction>();
    }
    inline JSFunction Bind(std::initializer_list<napi_value> args)
    {
        size_t argc = args.size();

        napi_value* argv = new napi_value[argc];
        auto iter = args.begin();

        napi_value thisVar = *iter;
        JSFunction result(env_, thisVar, value_);
        result.boundArgc_ = args.size() - 1;
        iter++;

        for (size_t i = 0; i < result.boundArgc_; i++) {
            argv[i] = *iter;
            iter++;
        }
        result.boundArgv_ = argv;

        return result.As<JSFunction>();
    }
    inline JSFunction Bind(JSValue thisVar, size_t argc, JSValue* args)
    {
        napi_value* argv = new napi_value[argc];

        JSFunction result(thisVar, *this);
        result.boundArgc_ = argc;

        for (size_t i = 0; i < result.boundArgc_; i++) {
            argv[i] = args[i];
        }
        result.boundArgv_ = argv;

        return result.As<JSFunction>();
    }
    inline JSFunction Bind(napi_value thisVar, size_t argc, napi_value* args) const
    {
        napi_value* argv = new napi_value[argc];

        JSFunction result(env_, thisVar, value_);
        result.boundArgc_ = argc;

        for (size_t i = 0; i < result.boundArgc_; i++) {
            argv[i] = args[i];
        }
        result.boundArgv_ = argv;

        return result;
    }
    inline JSValue Apply(std::initializer_list<JSValue> args)
    {
        return Bind(args).Call();
    }
    inline JSValue Apply(std::initializer_list<napi_value> args)
    {
        return Bind(args).Call();
    }
    inline JSValue Apply(JSValue thisVar, size_t argc, JSValue* args)
    {
        return Bind(thisVar, argc, args).Call();
    }
    inline JSValue Apply(napi_value thisVar, size_t argc, napi_value* args)
    {
        return Bind(thisVar, argc, args).Call();
    }

    inline operator napi_value() const { return MakeBound().value_; }
    inline JSValue GetBound() const { return MakeBound(); }
    inline JSString Name() { return Get("name").As<JSString>(); }

private:
    static void InitFunctionUtils(napi_env env);
    napi_value thisVar_ { nullptr };
    size_t boundArgc_ { 0 };
    napi_value* boundArgv_ { nullptr };

    thread_local static JSValueRef* g_functionBind;
    JSFunction MakeBound() const;
    friend class JSValueRef;
};

#undef IS_FUNCTION
#endif /* UTILS_JS_FUNCTION_H */
